#coding=utf-8
# Copyright 2011-2012 Maxim Petrov <maximpetrov@yahoo.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 3 or any later version.

import curses.ascii

def split_string(str, max_width):
    """
    Split string to list of substrings which lengths less or equal than max_width. Split by whitespaces
    or newline symbols. If some word length more than max_width then split this word.

    Keyword arguments:
        str - string to split
        max_width - max length of substring

    Return value:
        Return list of substrings which lengths less or equal than max_width
    """

    result = []
    prev = 0
    prev_white = -1
    for i, ch in enumerate(str):
        # split on newline in any case
        if ch == u'\n':
            result.append(str[prev:i].strip(u' '))
            prev = i + 1
            prev_white = i
        else:
            # remember position of last whitespace
            if ch == u' ':
                prev_white = i
            if i - prev >= max_width:
                # if any whitespace in last part split on last whitespace
                if prev < prev_white + 1:
                    result.append(str[prev:prev_white].strip(u' '))
                    prev = prev_white + 1
                # otherwise split word because it longer than max_width
                else:
                    result.append(str[prev:i].strip(u' '))
                    prev = i
                    prev_white = i - 1
    # append last part (if no newline at the end of string)
    if prev < len(str):
        result.append(str[prev:].strip(u' '))
    return result

class Control(object):
    """
    Abstract class. Parent of all controls. Implements common things.
    """


    def __init__(self, main_win, dimension):
        """
        Basic constructor of control

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
        """

        self.__focus = False
        self.__dimension = dimension
        self._main_window = main_win
        self.__par_x = 0
        self.__par_y = 0
        self.__par_width = 0
        self.__par_height = 0

    @property
    def rel_x(self):
        """Getter for x coordinate of control"""

        return self.__dimension(self.__par_width, self.__par_height)[0]

    @property
    def rel_y(self):
        """Getter for y coordinate of control"""

        return self.__dimension(self.__par_width, self.__par_height)[1]

    @property
    def x(self):
        """Getter for absolute x coordinate of control"""

        return self.__dimension(self.__par_width, self.__par_height)[0] + \
                                                                self.__par_x

    @property
    def y(self):
        """Getter for absolute y coordinate of control"""

        return self.__dimension(self.__par_width, self.__par_height)[1] + \
                                                                self.__par_y

    @property
    def width(self):
        """Getter for width of control"""

        return self.__dimension(self.__par_width, self.__par_height)[2]

    @property
    def height(self):
        """Getter for height of control"""

        return self.__dimension(self.__par_width, self.__par_height)[3]

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw control in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            widht - width of parent window
            height - height of parent window
        """

        if x is not None:
            self.__par_x = x
        if y is not None:
            self.__par_y = y
        if width is not None:
            self.__par_width = width
        if height is not None:
            self.__par_height = height

    @property
    def focus(self):
        """Getter for __focus"""

        return self.__focus

    @focus.setter
    def focus(self, focus):
        """Setter for __focus. Calls _focus()"""

        self.__focus = focus
        if self.main_win is not None:
            self._focus()

    def _focus(self):
        """Display state of focus."""
        pass

    def handle_keypress(self, ch, uni):
        """
        Function handles keypresses in field

        Keyword argument
            ch - code of pressed key or unicode symbol
            uni - True if ch is Unicode char, False otherwise
        """
        pass

    @property
    def main_win(self):
        """Getter for _main_win"""

        return self._main_window.window


    @main_win.setter
    def main_win(self, main_win):
        """Setter for _main_win"""

        self._main_window = main_win

class TextField(Control):
    """
    This class is used for show text field and work with it.
    """

    @property
    def main_win(self):
        """Getter for _main_win"""

        return self._main_window.window

    @main_win.setter
    def main_win(self, main_win):
        """Setter for _main_win"""

        self._main_window = main_win
        self.__pad.main_win = main_win

    def __init__(self, main_win, dimension, init_text=u''):
        """
        Constructor for text field.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            init_text - initial content of field
        """

        Control.__init__(self, main_win, dimension)
        # Content of field
        self.text = init_text
        # Pad for field
        self.__pad = Pad(None,
                    lambda w, h: tuple(list(dimension(w, h)[0:3]) + [1]),
                    lambda s, c: [(self._text_to_show(), curses.A_UNDERLINE, None)])
        self.__pad.bkg_attr = curses.A_UNDERLINE
        # Cursor position in text
        self.__cp = len(init_text)

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw field in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            widht - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        self.__pad.draw(x, y, width, height)
        self._focus()

    def __set_curs_pos(self, x, rel):
        """
        Function to move cursor in text field

        Keyword arguments:
            x - new cursor position or movement
            rel - if True then x means movement, else x means new position
        """

        if len(self.text) == 0:
            self.__cp = 0
            self.__pad.x_shift = 0
            return
        if rel:
            new_pos = self.__cp + x
        else:
            new_pos = x
        if new_pos < 0:
            new_pos = 0
        elif new_pos > len(self.text):
            new_pos = len(self.text)
        self.__cp = new_pos
        if new_pos - self.__pad.x_shift < 0:
            self.__pad.x_shift = new_pos
            self.__pad.draw()
        elif new_pos - self.__pad.x_shift > self.width:
            self.__pad.x_shift = new_pos - self.width
            self.__pad.draw()
        self._focus()

    def _text_to_show(self):
        """Return text to show"""

        return self.text

    def handle_keypress(self, ch, uni):
        """
        Function handles keypresses in field

        Keyword argument
            ch - code of pressed key or unicode symbol
            uni - True if ch is Unicode char, False otherwise
        """

        if uni or curses.ascii.isprint(ch):
            self.text = self.text[:self.__cp] + unichr(ch) + \
                        self.text[self.__cp:]
            self.__pad.draw()
            self.__set_curs_pos(1, True)
        elif ch == curses.KEY_LEFT:
            self.__set_curs_pos(-1, True)
        elif ch == curses.KEY_RIGHT:
            self.__set_curs_pos(1, True)
        elif ch == curses.KEY_HOME:
            self.__set_curs_pos(0, False)
        elif ch == curses.KEY_END:
            self.__set_curs_pos(len(self.text), False)
        elif ch == curses.KEY_BACKSPACE:
            if self.__cp > 0:
                self.text = self.text[:self.__cp - 1] + self.text[self.__cp:]
                self.__pad.draw()
                self.__set_curs_pos(-1, True)
        elif ch == curses.KEY_DC:
            self.text = self.text[:self.__cp] + self.text[self.__cp + 1:]
            self.__pad.draw()
            self._focus()

    def _focus(self):
        """Display change of focus."""

        if self.focus:
            curses.curs_set(1)
            y = self.y
            x = self.__cp - self.__pad.x_shift + self.x
            self.main_win.move(y, x)

class PasswordField(TextField):
    """
    This class is used for show text field with content and work with it.
    """

    def _text_to_show(self):
        """Return some number of asterisks"""

        return u'*' * len(self.text)

class Button(Control):
    """
    This class draws button.
    """

    def __init__(self, main_win, dimension, caption=u''):
        """
        Constructor for button.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            caption - caption of button
        """

        Control.__init__(self, main_win, dimension)
        self.__caption = caption

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw button in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            widht - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        self.main_win.addstr(self.y, self.x, '<' + self._main_window.str_to_external(self.__caption) + '>')
        self._focus()

    def _focus(self):
        """Display change of focus."""

        if self.focus:
            curses.curs_set(0)
            self.main_win.chgat(self.y, self.x - 1, len(self.__caption) + 4,
                               curses.A_REVERSE)
        else:
            self.main_win.chgat(self.y, self.x - 1, len(self.__caption) + 4,
                               curses.A_NORMAL)

class CheckBox(Control):
    """
    This class draws button.
    """

    def __init__(self, main_win, dimension, caption=u'', checked=False):
        """
        Constructor for checkbox.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            caption - caption of checkbox
            checked - initial state of checkbox
        """

        Control.__init__(self, main_win, dimension)
        self.__caption = caption
        self.checked = checked

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw checkbox in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            width - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        if self.checked:
            self.main_win.addstr(self.y, self.x, self._main_window.str_to_external(self.__caption)
                                    + ' [x]')
        else:
            self.main_win.addstr(self.y, self.x, self._main_window.str_to_external(self.__caption)
                                    + ' [ ]')
        self._focus()

    def _focus(self):
        """Display change of focus."""

        if self.focus:
            curses.curs_set(1)
            self.main_win.move(self.y, self.x + len(self.__caption) + 2)

    def handle_keypress(self, ch, uni):
        """
        Function handles keypresses in checkbox

        Keyword argument
            ch - code of pressed key or unicode symbol
            uni - True if ch is Unicode char, False otherwise
        """

        if ch == ord(' '):
            self.checked = not self.checked
            self.draw()

class Label(Control):
    """
    This class draws label.
    """

    def __init__(self, main_win, dimension, caption=u'', multiline=False):
        """
        Constructor for label.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            caption - caption of label
            multiline - draw label on some lines if needed (works only if
                        width in draw() is not None)
        """

        Control.__init__(self, main_win, dimension)
        self.__caption = caption
        self.__multiline = multiline

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw label in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            width - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        if width is None:
            self.main_win.addstr(self.y, self.x,
                            self._main_window.str_to_external(self.__caption))
        else:
            if not self.__multiline:
                self.main_win.addstr(self.y, self.x, self._main_window.
                                str_to_external(self.__caption[:self.width]))
            else:
                for i, s in enumerate(split_string(self.__caption, self.width)):
                    self.main_win.addstr(self.y + i, self.x,
                                         self._main_window.str_to_external(s))

class Pad(Control):
    """
    Class for replace pad from curses
    """

    def __init__(self, main_win, dimension, get_lines):
        """
        Constructor for pad.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            get_lines - function that return lines for print with attribute set
                        function takes 2 arguments:
                            first line index and lines count
                        function return item format:
                            (line, line_attr, [group_attr, ...]) where
                                line - string to print
                                line_attr - attributes for whole line
                                group_attr - attributes for part of line:
                                    (start, count, attr) where
                                        start - start of group
                                        count - count of symbols in group
                                        attr - attributes for group
        """

        Control.__init__(self, main_win, dimension)
        self._get_lines = get_lines
        self.x_shift = 0
        self.y_shift = 0
        self.bkg_attr = curses.A_NORMAL

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw pad in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            widht - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        self.clear()
        for i, (l, la, ga) in enumerate(self._get_lines(self.y_shift, self.height)):
            if i == self.height:
                break
            if la is not None:
                self.main_win.addstr(self.y + i, self.x,
                        self._main_window.str_to_external(l[self.x_shift:self.x_shift + self.width]), la)
            else:
                self.main_win.addstr(self.y + i, self.x,
                         self._main_window.str_to_external(l[self.x_shift:self.x_shift + self.width]))
            if ga is not None:
                for start, count, attr in ga:
                    st = start - self.x_shift
                    c = count
                    if st >= self.width:
                        continue
                    if st < 0:
                        c += st
                        st = 0
                    if c <= 0:
                        continue
                    if st + c > self.width:
                        c = self.width - st
                    self.main_win.chgat(self.y + i, self.x + st, c, attr)

    def clear(self):
        """Function clear area where pad located"""

        for i in range(0, self.height):
            self.main_win.addstr(self.y + i, self.x, u' ' * self.width, self.bkg_attr)

class TextArea(Control):
    """
    This class is used for show text area and work with it.
    """

    @property
    def main_win(self):
        """Getter for _main_win"""

        return self._main_window.window

    @main_win.setter
    def main_win(self, main_win):
        """Setter for _main_win"""

        self._main_window = main_win
        self.__pad.main_win = main_win

    @staticmethod
    def __get_pad_dimension(dimension):
        def dim(w, h):
            left, top, width, height = dimension(w, h)
            return (left + 1, top + 1, width - 2, height - 2)

        return dim

    @staticmethod
    def __split_lines(text, width):
        i = 0
        lines = []
        # Split text to lines.  Line split if '\n' appears ('\n' isn't deleted)
        # or if length of part becomes equal to pad width.
        while (i < len(text)):
            # Find '\n' not far than width from current position.
            ix = text.find(u'\n', i, i + width)
            if ix == -1:
                n_i = i + width
            else:
                n_i = ix + 1
            lines.append(text[i:n_i])
            i = n_i
        # If text ends with '\n' then add empty line
        # in order that we can move cursor to new line
        if text[-1:] == u'\n':
            lines.append(u'')
        return lines

    def __init__(self, main_win, dimension, title=u'', init_text=u'',
                    position=None):
        """
        Constructor for text area.

        Keyword arguments:
            main_win - MainWindow instance
            dimension - function calculates position of upper left corner,
                        width and height of control. This function takes
                        width and height of parent window
            title - title for area
            init_text - initial content of area
            position - initial position of cursor. None means end of init_text
        """

        Control.__init__(self, main_win, dimension)
        # Content of area
        self.text = init_text
        self.__text_lines = []
        self.title = title
        # Pad for area
        self.__pad = Pad(None, TextArea.__get_pad_dimension(dimension),
                    lambda s, c: ((l.split(u'\n')[0], None, None)
                                    for l in self.__text_lines[s:s+c]))
        if position is None:
            self.__cp = len(init_text)
        else:
            self.__cp = position

    def draw(self, x=None, y=None, width=None, height=None):
        """
        Function to draw field in specified location

        Keywords arguments:
            x, y - coordinate of upper left corner of parent window
            widht - width of parent window
            height - height of parent window
        """

        Control.draw(self, x, y, width, height)
        sw = self.main_win.subwin(self.height, self.width,
                                     self.y, self.x)
        self.__text_lines = TextArea.__split_lines(self.text, self.width - 2)
        sw.box()
        sw.addstr(0, 1, self._main_window.str_to_external(self.title[:self.width - 2]))
        self.__pad.draw(x, y, width, height)
        self._focus()

    def __get_curs_xy(self):
        """
        Function returns current cursor position as (x, y) not linear

        Return value:
            (x, y) - cursor coordinates.
        """

        # If text is empty then coordinates are (0, 0)
        if len(self.text) == 0:
            return (0, 0)
        pos = 0
        # Pass lines until sum of it's lengths less then linear cursor
        # position. y - index of last line that not break condition.
        # x - difference between linear cursor position and last sum of lengths
        for i, l in enumerate(self.__text_lines):
            new_pos = pos + len(l)
            if new_pos > self.__cp:
                return (self.__cp - pos, i)
            pos = new_pos
        # If sum of all lines lengths not greater than linear position
        # then current position in the end of text
        return (len(self.__text_lines[-1]), len(self.__text_lines) - 1)

    def __curs_move(self, dx, dy):
        """
        Function moves cursor by dx symbols in line and by dy lines

        Keyword arguments:
            dx - symbols to move by
            dy - lines to move by
        """

        # If text is empty then position is 0.
        if len(self.__text_lines) == 0:
            self.__cp = 0
            self._focus()
            return
        # Move only in line equals movement of __cp
        if dy == 0:
            new_pos = self.__cp + dx
            if new_pos < 0:
                new_pos = 0
            elif new_pos > len(self.text):
                new_pos = len(self.text)
            self.__cp = new_pos
            self._focus()
            return
        # If should move by dy lines then get x and y coordinates.
        ccp_x, ccp_y = self.__get_curs_xy()
        new_x = ccp_x + dx
        new_y = ccp_y + dy
        # If movement not meet in lines array bounds then move to nearest bound.
        if new_y < 0:
            new_y = 0
        elif new_y >= len(self.__text_lines):
            new_y = len(self.__text_lines) - 1
        # If movement not in line bounds then
        #   1. In case of movement by lines move to the nearest bound
        #   2. If there was only symbol movement then move to bound of
        #      respective neighbour line
        if new_x < 0:
            if dy == 0 and new_y > 0:
                new_y -= 1
                new_x = len(self.__text_lines[new_y]) - 1
            else:
                new_x = 0
        elif new_x >= len(self.__text_lines[new_y]):
            if dy == 0 and new_y < len(self.__text_lines) - 1:
                new_x = 0
                new_y += 1
            else:
                new_x = len(self.__text_lines[new_y])
                if self.__text_lines[new_y][-1:] == u'\n':
                    new_x -= 1
        # Calculate linear cursor position by sum lengths of all previous lines
        self.__cp = len(u''.join(self.__text_lines[:new_y])) + new_x
        self._focus()

    def handle_keypress(self, ch, uni):
        """
        Function handles keypresses in field

        Keyword argument
            ch - code of pressed key or unicode symbol
            uni - True if ch is Unicode char, False otherwise
        """

        # If pressed ordinal symbol than add it to text in linear position
        # and move position by 1 right.
        if uni or curses.ascii.isprint(ch) or ch == ord('\n'):
            self.text = self.text[:self.__cp] + unichr(ch) + \
                             self.text[self.__cp:]
            self.__text_lines = TextArea.__split_lines(self.text, self.__pad.width)
            self.__curs_move(1, 0)
            self.__pad.draw()
        # If arrow keys pressed then move cursor in same direction
        elif ch == curses.KEY_UP:
            self.__curs_move(0, -1)
        elif ch == curses.KEY_LEFT:
            self.__curs_move(-1, 0)
        elif ch == curses.KEY_DOWN:
            self.__curs_move(0, 1)
        elif ch == curses.KEY_RIGHT:
            self.__curs_move(1, 0)
        # If pressed Home or End then find nearest '\n' (forward or backward
        # respectively) and move cursor here.
        elif ch == curses.KEY_HOME:
            # rfind for first line return -1 as not found but -1 + 1 = 0 => OK
            self.__cp = self.text.rfind(u'\n', 0, self.__cp) + 1
        elif ch == curses.KEY_END:
            self.__cp = self.text.find(u'\n', self.__cp)
            if self.__cp == -1:
                self.__cp = len(self.text)
        # If BackSpace pressed then delete symbol before cursor and move cursor
        # one symbol back
        elif ch == curses.KEY_BACKSPACE:
            if self.__cp > 0:
                self.text = self.text[:self.__cp - 1] + self.text[self.__cp:]
                self.__text_lines = TextArea.__split_lines(self.text, self.__pad.width)
                self.__curs_move(-1, 0)
                self.__pad.draw()
        # If Delete pressed then delete symbol after cursor
        elif ch == curses.KEY_DC:
            self.text = self.text[:self.__cp] + self.text[self.__cp + 1:]
            self.__text_lines = TextArea.__split_lines(self.text, self.__pad.width)
            self.__pad.draw()
        self._focus()

    def _focus(self):
        """Display change of focus."""

        if self.focus:
            curses.curs_set(1)
            ccp_x, ccp_y = self.__get_curs_xy()
            rel_y = ccp_y - self.__pad.y_shift
            if rel_y < 0:
                self.__pad.y_shift += rel_y
            elif rel_y >= self.__pad.height:
                self.__pad.y_shift += rel_y - self.__pad.height + 1
            if rel_y != ccp_y - self.__pad.y_shift:
                self.__pad.draw()
            curses.curs_set(1)
            self.main_win.move(self.__pad.y + ccp_y - self.__pad.y_shift,
                                self.__pad.x + ccp_x)
